package com.vhall.component.aspect;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IoUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.vhall.component.entity.rbac.vo.RolePermissionConsoleRequestVO;
import com.vhall.component.entity.rbac.vo.RolePermissionConsoleResponseVO;
import com.vhall.component.framework.common.exception.BusinessException;
import com.vhall.component.plugin.common.annotation.TokenAuth;
import com.vhall.component.plugin.common.aspect.BaseDataAspect;
import com.vhall.component.plugin.common.constant.RedisKey;
import com.vhall.component.plugin.common.exception.BizErrorCode;
import com.vhall.component.plugin.common.support.Token;
import com.vhall.component.plugin.common.support.TokenThreadLocal;
import com.vhall.component.plugin.common.utils.JsonUtil;
import com.vhall.component.service.account.AccountService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

/**
 * token切面
 *
 * @author longzhen.gong
 * @date 2021/6/21 10:41
 */
@Slf4j
@Aspect
@Component
@Order(2)
public class TokenAspect extends BaseDataAspect {


    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private AccountService accountService;


    @Pointcut("@annotation(com.vhall.component.plugin.common.annotation.TokenAuth) || @within(com.vhall.component.plugin.common.annotation.TokenAuth)")
    public void pointCut() {
        // Do nothing
    }


    @Around("pointCut()")
    public Object tokenBefore(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法参数值数组
        Object[] args = joinPoint.getArgs();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Assert.notNull(servletRequestAttributes, "请求参数异常");
        HttpServletRequest request = servletRequestAttributes.getRequest();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Class<?> targetClass = joinPoint.getTarget().getClass();
        Method method = methodSignature.getMethod();
        TokenAuth tokenAuth = method.getAnnotation(TokenAuth.class);
        if (tokenAuth == null) {
            tokenAuth = targetClass.getAnnotation(TokenAuth.class);
        }
        boolean check = tokenAuth.check();
        String param = request.getHeader("token");

        Token tk;
        String tokenText = "";
        if (validMockFlag(request)) {
            tokenText = "{\"accountId\":14596,\"accountType\":1,\"appId\":\"fGWLbDnj\",\"deviceId\":\"\",\"modules\":[\"Console\",\"Api\"],\"nickname\":\"尚记学\",\"orgCode\":\"2999\",\"orgName\":\"佛山禅城分公司\",\"phone\":13632906105,\"roleId\":11,\"roleType\":1,\"status\":0,\"headquartersMarkerFlag\":true,\"guoXinAccountType\":3,\"username\":\"尚记学\"}";
        } else {
            if (StringUtils.isAllEmpty(param) && check) {
                throw new BusinessException(BizErrorCode.AUTH_LOGIN_TOKEN_INVALID);
            }
            if (StringUtils.isAllEmpty(param)) {
                return joinPoint.proceed(args);
            }

            String token = (String) redisTemplate.opsForValue().get(RedisKey.AUTH_TOKEN + param);
            if (StringUtils.isBlank(token) && check) {
                throw new BusinessException(BizErrorCode.AUTH_LOGIN_TOKEN_INVALID);
            }

            if (StringUtils.isBlank(token)) {
                return joinPoint.proceed(args);
            }
            tokenText = JSON.parse(token).toString();

        }

        try {
            tk = JsonUtil.objectFromJSONString(tokenText, Token.class);
            TokenThreadLocal.setToken(tk);
            checkAccountRoleAction(request);
            return joinPoint.proceed(args);
        } catch (Throwable e) {
            log.error("", e);
            throw e;
        } finally {
            TokenThreadLocal.remove();
        }
    }

    /**
     * 检查用户是否有用户角色
     *
     * @param request
     */
    private void checkAccountRoleAction(HttpServletRequest request) {
        RolePermissionConsoleRequestVO vo = new RolePermissionConsoleRequestVO();
        String path = request.getRequestURI();
        if (permissionUrls.contains(path)) {
            List<RolePermissionConsoleResponseVO> roleMenus = accountService.rolePermissionConsole(vo);
            List<Integer> roleMenuIds = getMenuIdsByStream(roleMenus);
            Map<String, List<Integer>> urlToMenuIdsMap =
                    roleMenuActions.stream()
                            .collect(Collectors.toMap(
                                    RoleAction::getUrl,
                                    action -> {
                                        List<Integer> tempMenuIds = new ArrayList<>();
                                        tempMenuIds.add(action.getMenuId());
                                        return tempMenuIds;
                                    },
                                    (menuIds1, menuIds2) -> {
                                        menuIds1.addAll(menuIds2);
                                        return menuIds1;
                                    }
                            ));
            List<Integer> pathMenuIdList = urlToMenuIdsMap.get(path);
            if (CollUtil.isEmpty(roleMenuIds.stream().filter(pathMenuIdList::contains).collect(Collectors.toList()))) {
                throw new BusinessException(BizErrorCode.BIZ_NO_PERMISSION);
            }
        }
    }

    /**
     * 对用户角色菜单进行解析
     *
     * @param roleMenus
     * @return
     */
    private List<Integer> getMenuIdsByStream(List<RolePermissionConsoleResponseVO> roleMenus) {
        return roleMenus.stream()
                .flatMap(menu -> {
                    List<Integer> menuIds = new ArrayList<>();
                    menuIds.add(menu.getMenuId());

                    if (menu.getChildren() != null && !menu.getChildren().isEmpty()) {
                        menuIds.addAll(getMenuIdsByStream(menu.getChildren()));
                    }
                    return menuIds.stream();
                }).distinct().collect(Collectors.toList());

    }

    /**
     * 解析菜单对应接口权限
     */
    private static JSONArray permissionJson;
    /**
     * 需要鉴权的 url 和 url 绑定的对应菜单
     */
    private static List<RoleAction> roleMenuActions;
    /**
     * 需要进行鉴权的 url
     */
    private static Set<String> permissionUrls;

    static {
        try {
            ClassPathResource classPathResource = new ClassPathResource("permission.json");
            InputStream stream = classPathResource.getInputStream();
            byte[] bytes = IoUtil.readBytes(stream, true);
            String permission = new String(bytes);
            permissionJson = JSONArray.parseArray(permission);
            if (Objects.nonNull(permissionJson)) {
                if (!permissionJson.isEmpty()) {
                    roleMenuActions = permissionJson.toJavaList(RoleAction.class);
                    permissionUrls = roleMenuActions.stream().map(RoleAction::getUrl).collect(Collectors.toSet());
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex.getMessage());
        }
    }

    /**
     * 菜单和 url 的关系
     */
    @Data
    static class RoleAction {
        private Integer menuId;
        private String des;
        private String url;
        @JsonProperty("url_desc")
        private String urlDesc;
    }

}
